package com.easy.frame.base.service.base;

import cn.hutool.core.exceptions.ExceptionUtil;
import com.alibaba.fastjson.JSON;
import com.easy.frame.base.basic.ServiceResponse;
import com.easy.frame.base.basic.constant.DbConstants;
import com.easy.frame.base.basic.enums.ResponseCodeEnum;
import com.easy.frame.base.basic.exception.ServiceException;
import com.easy.frame.base.basic.exception.UnauthenticatedException;
import com.easy.frame.base.basic.exception.UnauthorizedException;
import com.easy.frame.base.basic.pojo.SysExceptionLog;
import com.easy.frame.base.basic.util.AuthUtils;
import com.easy.frame.base.service.redis.stream.RedisStreamProducer;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.time.Duration;
import java.util.Date;
import java.util.Enumeration;
import java.util.UUID;

/**
 * 全局异常处理类
 *
 * @author zzf
 * @date 2020/7/6 9:47
 */
@Slf4j
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(ServiceException.class)
    public ServiceResponse<?> handlerServiceException(ServiceException e) {
        return new ServiceResponse<>(ResponseCodeEnum.PARAM_ERROR.getCode(), e.getMessage());
    }

    /**
     * 参数校验异常
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(ConstraintViolationException.class)
    public ServiceResponse<?> handlerConstraintViolationException(ConstraintViolationException e) {
        return new ServiceResponse<>(ResponseCodeEnum.PARAM_ERROR.getCode(), e.getMessage());
    }


    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ServiceResponse<?> handlerMissParameterException(MissingServletRequestParameterException e) {
        e.printStackTrace();
        return new ServiceResponse<>(ResponseCodeEnum.PARAM_ERROR, e.getMessage());
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public ServiceResponse<?> handlerException(Exception e, HttpServletRequest httpRequest) {
        e.printStackTrace();
        try {
            persistentExceptionAsync(e, httpRequest);
        } catch (Exception exception) {
            //do nothing
        }
        return new ServiceResponse<>(e);
    }

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthenticatedException.class)
    public ServiceResponse<?> handlerUnauthorizedException(UnauthenticatedException e) {
        return new ServiceResponse<>();
    }


    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(UnauthorizedException.class)
    public ServiceResponse<?> handlerUnauthorizedException(UnauthorizedException e) {
        return new ServiceResponse<>(ResponseCodeEnum.AUTH_ERROR.getCode(), e.getMessage());
    }

    /**
     * 缓存一个小时内的异常信息，已发生过的异常不处理
     */
    private final Cache<String, String> cache = Caffeine.newBuilder()
            .expireAfterAccess(Duration.ofHours(1))
            .build();

    public void persistentExceptionAsync(Exception e, HttpServletRequest request) {
        if (checkDuplicate(request)) {
            return;
        }
        SysExceptionLog exceptionLog = new SysExceptionLog()
                // 设置异常字段
                .setExceptionName(e.getClass().getSimpleName())
                .setExceptionMessage(ExceptionUtil.getMessage(e))
                .setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e))
                .setExceptionStackTrace(ServiceResponse.getExceptionTrace(e))
                // 设置其它字段
                .setTraceId(UUID.randomUUID().toString())
                .setUri(request.getRequestURI())
                .setQueryString(buildQueryString(request))
                .setMethod(request.getMethod())
                .setUserAgent(getUserAgent(request))
                .setIp(getIp(request))
                .setCreateTime(new Date())
                .setUserId(AuthUtils.getCurrentUserId());

        if (exceptionLog.getExceptionMessage().length() > DbConstants.EXCEPTION_LENGTH) {
            exceptionLog.setExceptionMessage(exceptionLog.getExceptionMessage()
                    .substring(DbConstants.EXCEPTION_LENGTH_START, DbConstants.EXCEPTION_LENGTH));
        }
        if (exceptionLog.getExceptionRootCauseMessage().length() > DbConstants.EXCEPTION_LENGTH) {
            exceptionLog.setExceptionRootCauseMessage(exceptionLog.getExceptionRootCauseMessage()
                    .substring(DbConstants.EXCEPTION_LENGTH_START, DbConstants.EXCEPTION_LENGTH));
        }
        if (exceptionLog.getExceptionStackTrace().length() > DbConstants.EXCEPTION_LENGTH) {
            exceptionLog.setExceptionStackTrace(exceptionLog.getExceptionStackTrace()
                    .substring(DbConstants.EXCEPTION_LENGTH_START, DbConstants.EXCEPTION_LENGTH));
        }

        StackTraceElement stackTraceElement = e.getStackTrace()[0];
        exceptionLog.setExceptionClassName(stackTraceElement.getClassName());
        exceptionLog.setExceptionFileName(stackTraceElement.getFileName());
        exceptionLog.setExceptionMethodName(stackTraceElement.getMethodName());
        exceptionLog.setExceptionLineNumber(stackTraceElement.getLineNumber());

        log.error("happened exception: " + JSON.toJSONString(exceptionLog));
        try {
            RedisStreamProducer.sendExceptionLogMsg(exceptionLog);
        } catch (Exception exception) {
            log.error("send exception log fail~~~");
        }
        cacheExceptionLocal(request, exceptionLog);
    }

    /**
     * 缓存到本地
     */
    private void cacheExceptionLocal(HttpServletRequest request, SysExceptionLog exceptionLog) {
        cache.put(getCacheKey(request), exceptionLog.simpleInfo());
    }

    /**
     * 判断是否重复异常
     * 是则打印log并结束流程
     */
    private boolean checkDuplicate(HttpServletRequest request) {
        String exceptionLog = cache.getIfPresent(getCacheKey(request));
        if (exceptionLog != null) {
            log.error("happened same exception: " + exceptionLog);
            return true;
        }
        return false;
    }

    private final String chart = "-";

    private String getCacheKey(HttpServletRequest request) {
        return request.getMethod() + chart + request.getRequestURI();
    }

    private String getIp(HttpServletRequest request) {
        // 基于 X-Forwarded-For 获得
        String ip = request.getHeader("X-Forwarded-For");
        if (!StringUtils.isEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值，第一个 ip 才是真实 ip
            int index = ip.indexOf(",");
            if (index != -1) {
                return ip.substring(0, index);
            } else {
                return ip;
            }
        }
        // 基于 X-Real-IP 获得
        ip = request.getHeader("X-Real-IP");
        if (!StringUtils.isEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            return ip;
        }
        // 默认方式
        return request.getRemoteAddr();
    }

    /**
     * @param request 请求
     * @return ua
     */
    private String getUserAgent(HttpServletRequest request) {
        String ua = request.getHeader("User-Agent");
        return ua != null ? ua : "";
    }

    /**
     * 根据request拼接queryString
     *
     * @return queryString
     */
    private String buildQueryString(HttpServletRequest request) {
        Enumeration<String> es = request.getParameterNames();
        if (!es.hasMoreElements()) {
            return "";
        }
        String parameterName, parameterValue;
        StringBuilder params = new StringBuilder();
        while (es.hasMoreElements()) {
            parameterName = es.nextElement();
            parameterValue = request.getParameter(parameterName);
            params.append(parameterName).append("=").append(parameterValue).append("&");
        }
        return params.deleteCharAt(params.length() - 1).toString();
    }

}
